package com.zkh.myutils.net.upload;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import com.zkh.myutils.io.file.FileUtils;
import com.zkh.myutils.io.log.Logger;
import com.zkh.myutils.system.UploadConfigBean;
import com.zkh.myutils.utils.PathUtils;
import com.zkh.myutils.utils.Regexp;
import com.zkh.myutils.utils.StringUtils;
import com.zkh.myutils.utils.Utils;

/**
 * 文件上传解析器
 * @author zkh
 */
public class Uploader {
	
	/**
	 * 上传对象唯一标识。<br/>
	 * 该类在保存上传进度时，会以一个Map保存在session中，Map的键，即为该属性值。这是为了避免在多个页面同时上传时，出现进度信息互相干扰的情况。
	 */
	private String key;
	
	/**
	 * 构造方法
	 */
	public Uploader() {
		this(Utils.getUUID());
	}
	
	/**
	 * 构造方法
	 * @param key 上传对象唯一标识，用来获取上传进度
	 */
	public Uploader(String key) {
		this.key = key;
	}
	
	/**
	 * 检查是否是文件上传操作（multipart/form-data）
	 * @param request
	 */
	public static boolean isUpload(HttpServletRequest request) {
		//获取contentType
		String contentType = request.getContentType();
		//检查格式
		return contentType!=null && contentType.indexOf("multipart/form-data")>-1;
	}
	
	/**
	 * 解析上传数据。使用默认配置。
	 * @param request
	 * @return
	 * @throws UploadException
	 */
	public List<FileItem> parseUpload(HttpServletRequest request) throws UploadException{
		return parseUpload(request, UploadConfigBean.getConfig(), false);
	}
	
	/**
	 * 解析上传数据
	 * @param request
	 * @param config 上传文件自定义配置
	 * @return
	 * @throws UploadException 
	 */
	public List<FileItem> parseUpload(HttpServletRequest request, UploadConfigBean config) throws UploadException{
		return parseUpload(request, config, false);
	}
	
	/**
	 * 解析上传数据。使用默认配置。
	 * @param request
	 * @param saveProgress 是否保存上传进度。true：要保存上传进度。通过Uploader.getProgress()方法获取。
	 * @return
	 * @throws UploadException
	 */
	public List<FileItem> parseUpload(HttpServletRequest request, boolean saveProgress) throws UploadException{
		return parseUpload(request, UploadConfigBean.getConfig(), false);
	}
	
	/**
	 * 解析上传数据
	 * @param request
	 * @param saveProgress 是否保存上传进度。true：要保存上传进度。通过Uploader.getProgress()方法获取。
	 * @return
	 * @throws UploadException
	 */
	public List<FileItem> parseUpload(HttpServletRequest request, UploadConfigBean config, boolean saveProgress) throws UploadException {
		try {
			/** 前置检查 **/
			//无配置
			if(config==null) {
				throw new UploadException("上传配置对象UploaderConfig不能为null");
			}
			//获取上传相关流
			InputStream is = request.getInputStream();
			//上传总大小
			long totalSize = Long.parseLong(request.getHeader("content-length"));
			//上传数据过大
			if(config.getMaxSize()!=0 && config.getMaxSize()<totalSize) {
				throw new UploadException("上传数据大于配置的最大上传大小"+config.getMaxSize());
			}
			//最终解析容器
			List<FileItem> fileItemList = new ArrayList<FileItem>();
			
			/** 初始化变量，解析时需要用到 **/
			Variable var = new Variable(request, config, saveProgress, totalSize);
			/** 正式解析上传文件 **/
			//临时数据(read方法最大只能读取8192)
			byte[] bs = new byte[8192];
			int len = 0;
			while((len=is.read(bs))>-1) {
				//获取最新数据
				byte[] temp= combineArray(var.undeals, bs, len, var.rnidxList);
				//检查分隔符。注：request.getContentType()中的分隔符与实际不一样，不能用作分隔符
				if(var.separator==null) {
					var.separator = Arrays.copyOfRange(temp, 0, var.rnidxList.get(0)-1);
				}
				//开始索引
				int startIndex = 0;
				//处理换行
				for(int idx: var.rnidxList) {
					//截取
					byte[] sub = Arrays.copyOfRange(temp, startIndex, idx + 1);
					//处理各阶段
					switch(var.stage) {
						//1：查找分隔符阶段
						case 1:
							//如果是分隔符，改变阶段
							if(isMatched(sub, var.separator)) var.stage = 2;
							break;
						//2：查找表单类型阶段
						case 2:
							//有表单类型（没有表单类型，说明上传数据已解析完成）
							if(sub.length>0) {
								//创建表单对象
								var.item = new FileItem();
								//将字节转换为字符
								String t = new String(sub);
								//获取表单字段名
								var.item.setFieldname(Regexp.getMatchOne("(?<=name=\").*?(?=\")", t));
								//存在
								if((var.find = fileItemList.indexOf(var.item))>-1) {
									var.item = fileItemList.get(var.find);
								}
								//文件名
								String fname = Regexp.getMatchOne("(?<=filename=\").*?(?=\")", t);
								//有文件
								if(fname!=null) {
									//为空
									if(fname.isEmpty()) {
										var.item.setFile(true);	//文件标识
										var.item.addUploadFile(null);	//初始化容器，避免出现null
										fileItemList.add(var.item);		//保存表单
										var.stage = 1;	//查找分隔符
									}else {
										//文件后缀
										String suffix = FileUtils.getSuffix(fname);
										//创建上传文件对象
										var.item.addUploadFile(new UploadFile());
										var.item.setFile(true);	//文件标识
										var.item.getLastUploadFile().setFilename(FileUtils.getFilename(fname));	//文件名
										var.item.getLastUploadFile().setExtensionName(suffix);	//后缀
										//有后缀要求
										if(var.config.getAllowType().length>0) {
											//检查是否有后缀名
											if(suffix.isEmpty()){
												throw new UploadException("无法识别文件类型");
											}
											//检查后缀名是否符合要求
											if(!StringUtils.isExist(suffix, true, config.getAllowType())){
												throw new UploadException("文件类型" + suffix + "不允许上传");
											}
										}
										//打开文件写句柄
										if(config.isUseTempFile()) {//启用缓存文件的情况
											var.os = new FileOutputStream(var.tempFile = PathUtils.getTempFile());
				
										}else {
											var.os = new ByteArrayOutputStream();
										}
										var.type = 2; //文件
										var.stage = 3;	//修改阶段
									}
								}//表单值
								else {
									//添加空值
									var.item.addFieldValue("");
									var.type = 1; //表单
									var.breakEmpty = true;	//跳过空行
									var.stage = 4;	//修改阶段
								}
							}
							break;
						//3：查找文件类型阶段
						case 3:
							//将字节转换为字符，获取文件类型
							var.item.getLastUploadFile().setMimeType(new String(Arrays.copyOf(sub, sub.length-2)).substring(14));
							//阶段
							var.stage = 4;
							//跳过空行
							var.breakEmpty = true;
							break;
						//4：查找表单数据或文件数据阶段
						default:
							//找到分隔符，修改阶段。且如果是上传文件，则关闭文件流
							if(isMatched(sub, var.separator)) {
								//保存上次数据
								saveData(var, true);
								//开始查找下一个表单数据
								var.stage = 2;
								//文件，关闭流
								if(var.type==2) {
									//缓存文件
									if(config.isUseTempFile()) {
										var.os.close();
										//生成临时文件
										var.item.getLastUploadFile().setFile(var.tempFile);
									}else {
										//生成输入流
										var.item.getLastUploadFile().setFileStream(new ByteArrayInputStream(((ByteArrayOutputStream) var.os).toByteArray()));
										//关闭流
										var.os.close();
									}
									//大小
									var.item.getLastUploadFile().setSize(var.fileSize);
									//重置变量
									var.fileSize = 0;
								}
								//保存上传项
								if(var.find==-1) fileItemList.add(var.item);
								//重置临时数据
								var.tempContent = new byte[0];
							}
							//不是分隔符，解析数据
							else {
								//保存上次数据
								saveData(var, false);
								//获取本次数据
								if(var.breakEmpty) {
									var.breakEmpty = false;
								}else {
									var.tempContent = sub;
								}
							}
							break;
					}
					//重置开始索引
					startIndex = idx + 1;
				}
				//重置未处理元素
				if(startIndex>0) {
					var.undeals = Arrays.copyOfRange(temp, startIndex, temp.length);
				}
			}
			//返回最终数据
			return fileItemList;
		} catch (IOException e) {
			Logger.error(e);
			throw new UploadException("上传文件解析失败");
		}
	}
	
	
	/**
	 * 获取文件上传进度。如果没有上传进度记录，返回0.0 <br />
	 * <b>实际应用中，可能存在已上传文件大小大于总上传文件大小，此时返回值将大于1.0</b><br/><br/>
	 * <b>注意：该方法没有传上传对象唯一标识值（即key值），故只适用于文件上传请求不同时进行的情况。如果多个页面同时在上传文件，则获取的进度只会是多个文件上传中的随机一个进度。这时候要获取具体的某一个，需要传递key值。</b>
	 * @return
	 */
	public static double getProgress(HttpServletRequest request) {
		return getProgress(request, null);
	}
	
	/**
	 * 获取文件上传进度。如果没有上传进度记录，返回0.0 <br />
	 * <b>实际应用中，可能存在已上传文件大小大于总上传文件大小，此时返回值将大于1.0</b>
	 * @param request
	 * @param key 上传对象唯一标识。<b>如果能确定用户肯定不会在两个页面同时上传文件，那么该key值可以不指定，因为session中只有一个进度值，将直接取仅有的这个值。</b>
	 * @return
	 */
	public static double getProgress(HttpServletRequest request, String key) {
		//上传进度
		@SuppressWarnings("unchecked")
		Map<String, long[]> progressMap = (Map<String, long[]>) request.getSession().getAttribute("UploaderProgress");
		//上传进度为空，返回0
		if(progressMap==null || progressMap.isEmpty()) {
			return 0.0;
		}
		//上传进度容器
		long[] progress;
		//获取上传进度
		if(StringUtils.isNotEmpty(key)) {
			//指定了key
			progress = progressMap.get(key);
		}else {
			//未指定key，默认取第一条
			progress = progressMap.values().iterator().next();
		}
		//返回
		return progress==null || progress[1]==0 ? 0.0 : (progress[0] * 1.0/progress[1]);
	}
	
	/**
	 * 清除某个上传进度（当前session）
	 * @param request
	 */
	public static void clearProgress(HttpServletRequest request, String key) {
		//上传进度
		@SuppressWarnings("unchecked")
		Map<String, long[]> progressMap = (Map<String, long[]>) request.getSession().getAttribute("UploaderProgress");
		//清除指定进度
		if(progressMap!=null && StringUtils.isNotEmpty(key)) {
			progressMap.remove(key);
		}
	}
	
	/**
	 * 清除所有上传进度（当前session）
	 * @param request
	 */
	public static void clearProgressAll(HttpServletRequest request) {
		request.getSession().removeAttribute("UploaderProgress");
	}
	
	/**
	 * 保存数据
	 * @param var
	 * @param end
	 * @throws UploadException
	 * @throws IOException
	 */
	private void saveData(Variable var, boolean end) throws UploadException, IOException {
		//保存上次数据
		if(var.tempContent.length>0) {
			if(end) {
				//去掉末尾的换行符
				var.tempContent = Arrays.copyOf(var.tempContent, var.tempContent.length-2);
			}
			//表单
			if(var.type==1) {
				var.item.addLastFieldValue(new String(var.tempContent));
			}else {
				//统计文件大小
				var.fileSize += var.tempContent.length;
				//是否超过最大限制
				if(var.config.getSingleMaxSize()!=0 && var.config.getSingleMaxSize()<var.fileSize){
					clearProgress(var.request, this.key);
					//上传数据过大
					throw new UploadException("单个上传文件大于配置的最大单个文件上传大小"+var.config.getSingleMaxSize());
				}
				//文件
				var.os.write(var.tempContent);var.os.flush();
			}
			//计算上传大小，用以生成上传进度
			var.uploaded += var.tempContent.length;
			//保存进度
			if(var.saveProgress) {
				saveProgress(var.request, var.uploaded, var.totalSize);
			}
		}
	}
	
	/**
	 * 保存上传进度
	 * @param request
	 * @param saved
	 * @param total
	 */
	private void saveProgress(HttpServletRequest request, long saved, long total) {
		//上传进度
		@SuppressWarnings("unchecked")
		Map<String, long[]> progressMap = (Map<String, long[]>) request.getSession().getAttribute("UploaderProgress");
		//如果为null
		if(progressMap==null) {
			//初始化
			progressMap = new HashMap<String, long[]>();
			//保存session
			request.getSession().setAttribute("UploaderProgress", progressMap);
		}
		//获取进度
		long[] progress = progressMap.get(this.key);
		//为空
		if(progress==null) {
			progressMap.put(this.key, new long[] {saved, total});
		}else {
			progress[0] = saved;progress[1]=total;
		}
	}

	/**
	 * 检查是否是分隔符
	 * @param sub 行数据
	 * @param separator 分隔符
	 */
	private static boolean isMatched(byte[] sub, byte[] target) {
		//长度
		int len1 = sub.length, len2 = target.length;
		//不等
		if(len1<len2) {
			return false;
		}
		boolean eq = true;
		//检查每个字符是否相等
		for(int i=0; i<len2; i++) {
			//不相等
			if(sub[i] != target[i]) {
				eq = false;break;
			}
		}
		//检查
		return eq;
	}

	/**
	 * 合并数组
	 * @param undeal 未处理数组
	 * @param bs 已读取到的字节数组
	 * @param len 已读取字节数组的有效长度
	 * @param rnidxList 换行符索引
	 * @return
	 */
	private static byte[] combineArray(byte[] undeal, byte[] bs, int len, List<Integer> rnidxList) {
		//数组长度
		int unlen = undeal.length;
		//组合数组长度
		int nlen = unlen + len;
		//新数组
		byte[] bts = new byte[nlen];
		//初始化换行符索引
		if(rnidxList.size()>0) rnidxList.clear();
		//合并
		int i=0;
		//遍历
		for(;i<unlen;i++) {
			bts[i] = undeal[i];
		}
		//遍历
		for(;i<nlen;i++) {
			bts[i] = bs[i-unlen];
			//换行符
			if(bts[i]=='\n') {
				rnidxList.add(i);
			}
		}
		//返回组装后的数据
		return bts;
	}

}
